<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher;

utils\HttpHelper::appendResponseHeader('X-Powered-By', Switcher::VERSION);

/**
 * This class is the main class of the Switcher Framework. It acts as the orchestrator
 * of the framework through all phases. The <b>Switcher</b> class relies on the MVC
 * (Model-View-Controller) and IoC (Inversion of Control) patterns.
 * 
 * <p>The MVC pattern is implemented such as a user request leads to an action, in a
 * <i>Controller</i>, which role is to build a context, the <i>Model</i>. The context is
 * given to a <b>Renderer</b> which will retrieve the <i>View</i> to the user, according
 * to data found in the context. <i>Controllers</i> focus only on business logic, and
 * you can use built-in <i>Renderers</i> or write your own if needed.</p>
 * 
 * <p>Additionnaly, two other phases allow you to finely tune your application and have a
 * better control over the workflow. The Switcher framework allow you easily to filter the
 * request and for instance rewrite the URL before being forwarded to the requested controller.
 * You can also filter the response to the user and, for instance, set additionnal HTTP
 * headers. There is for example one built-in <b>Filter</b> which can compress the output.
 * <i>Filters</i> focus only on transversal technical issues.</p>
 * 
 * <p>The Switcher framework goes through all the phases in this order :
 * <ul>
 * 	<li>Filter the request <i>(from the first to the last Filter)</i></li>
 * 	<li>Find the requested view</li>
 * 	<li>Execute the corresponding action <i>(which populates the Context)</i></li>
 * 	<li>Render the view <i>(with data from the Context)</i></li>
 * 	<li>Filter the response <i>(from the last to the first Filter)</i></li>
 * </ul>
 * </p>
 * 
 * <p>The IoC pattern used by Switcher introduce one major design principle. You don't
 * have to instanciate your classes and initialize their properties, Switcher does it for
 * you. All the user classes extending Switcher technical classes (such as Controller, Renderer
 * or Filter) are initialized by the framework when needed. You basically only need to do
 * this in your main page to use the framework :
 * <code>
 * echo switcher\Switcher::newInstance($config)->doRequest();
 * </code>
 * The rest is handled in an IoC fashion. Switcher doesn't include manually other PHP files.
 * It relies rather on lazy loading with the PHP late static binding feature. Managed classes
 * must have a no-arg constructor to be instanciable by the framework.</p>
 * 
 * <p>The Switcher framework also handles several HTTP statuses. Several exceptions can be
 * thrown to deliver the user a special response. The exception can be thrown within any
 * phase to switch to a special response case. Other exceptions and exceptions thrown
 * within a special response case switch to the error response case (HTTP status : 500).
 * <br/>
 * Example, a ShowArticleController where the requested article might not exist :
 * <pre><code>
 * [...]
 * 
 * $article = $this->findArticle($articleId);
 * 
 * if ($article == null)
 * {
 *     throw new \switcher\exceptions\NotFoundException("Article not found : $articleId");
 * }
 * 
 * [...]
 * </code></pre>
 * </p>
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher
 * @since 1.0.0
 * 
 * @see switcher\config\Config
 * @see switcher\config\View
 * @see switcher\controllers\Controller
 * @see switcher\renderers\Renderer
 * @see switcher\filters\Filter
 * @see switcher\filters\CompressionFilter
 * @see switcher\exceptions\BadRequestException
 * @see switcher\exceptions\ForbiddenException
 * @see switcher\exceptions\ForwardException
 * @see switcher\exceptions\NotFoundException
 * @see switcher\exceptions\RedirectException
 */
final class Switcher
{
	const VERSION = 'Switcher/1.0.0-dev';
	
	/**
	 * The Switcher configuration
	 * @var switcher\config\Config
	 */
	private $config;
	
	/**
	 * The filter stack from the configuration
	 * @var switcher\filters\FilterStack
	 */
	private $filterStack;
	
	/**
	 * The current view
	 * @var switcher\config\View
	 */
	private $view;
	
	/**
	 * The current view's name
	 * @var string
	 */
	private $viewName;
	
	/**
	 * Logger.
	 * @var switcher\log\Logger
	 */
	private $logger;
	
	/**
	 * Hidden constructor
	 */
	private function __construct()
	{
	}
	
	/**
	 * Static factory for Switcher instanciation. This allow to chain the call to
	 * <i>doRequest</i> which cannot be done with a constructor.
	 * 
	 * @param $config A Switcher configuration
	 * @return Switcher A Switcher instance
	 * @throws \InvalidArgumentException If $config is null
	 * @see switcher\config\Config
	 */
	public static function newInstance(config\Config $config)
	{
		if ($config == null)
		{
			throw new \InvalidArgumentException('Null configuration');
		}
		
		$switcher = new Switcher();
		
		$switcher->config = $config;
		$switcher->filterStack = new filters\FilterStack($config);
		
		log\LoggerFactory::injectLogger($switcher);
		
		return $switcher;
	}
	
	/**
	 * <p>This is the main endpoint of the Switcher framework. The <b>doRequest</b>
	 * method is the one called to process a user's request once a <b>Switcher</b>
	 * object has been sucessfully created.</p>
	 * <p>This method goes through all the phases of the framework.</p>
	 */
	public function doRequest()
	{
		try
		{
			try
			{
				$this->removeRequestRoot();
				$this->filterStack->doFilterRequest();
				
				utils\HttpHelper::prepareGetParameters();
				
				$this->viewName = $this->config->findView();
				$this->view = $this->config->getView($this->viewName);
				$this->view->checkRequest();
				
				return $this->doAction();
			}
			catch(exceptions\ForwardException $e)
			{
				return $this->handleForwardException($e);
			}
			catch(exceptions\RedirectException $e)
			{
				return $this->handleRedirectException($e);
			}
			catch(exceptions\HttpException $e)
			{
				return $this->handleHttpException($e);
			}
		}
		catch(\Exception $e)
		{
			return $this->handleHttpException( new exceptions\InternalErrorException($e) );
		}
	}
	
	/**
	 * Remove the root from the request URI. The request can then be safely used as a relative
	 * path anywhere in the framework.
	 * 
	 * @throws exceptions\NotFoundException If the declared root does'nt match the request URI's root
	 */
	private function removeRequestRoot()
	{
		$root = $this->config->root();
		
		if (substr_compare($_SERVER['REQUEST_URI'], $root, 0, strlen($root)) !== 0)
		{
			throw new exceptions\NotFoundException('Invalid root : ' . $root);
		}
		
		$_SERVER['REQUEST_URI'] = substr($_SERVER['REQUEST_URI'], strlen($root));
	}
	
	/**
	 * This method actually renders the output. Goes through the business logic,
	 * view rendering and response filtering phases.
	 */
	private function doAction()
	{
		$controller = $this->view->controller;
		$renderer = $this->view->renderer;
		
		// render the view
		$controller->{$this->view->action}();
		$response = $renderer->doRendering();
		
		// everything went well, set the content type
		$renderer->setContentType();
		
		// go through the response filter phase
		return $this->filterStack->doFilterResponse($response);
	}
	
	/**
	 * Redirect the user agent to another view without HTTP notification.
	 * 
	 * @param exceptions\ForwardException $e The forward exception
	 */
	private function handleForwardException(exceptions\ForwardException $e)
	{
		$this->logger->info('ForwardException : ' . $e->getMessage());
		
		$this->viewName = $e->getView();
		$this->view = $this->config->getView($this->viewName);
		
		return $this->doAction();
	}
	
	/**
	 * Redirect the user agent (HTTP status 301 or 302) to another URL.
	 * 
	 * @param RedirectException $e The redirection exception
	 */
	private function handleRedirectException(exceptions\RedirectException $e)
	{
		$this->logger->info('RedirectException : ' . $e->getUrl());
		
		utils\HttpHelper::setResponseLocation( $e->getUrl() );
		utils\HttpHelper::setResponseStatus( $e->getCode() );
		
		return $this->filterStack->doFilterResponse('');
	}
	
	/**
	 * Load a special view (HTTP status != 200) and render it.
	 * 
	 * @param HttpException $e The HTTP exception
	 * @see switcher\config\Config#$badRequestView
	 * @see switcher\config\Config#$forbiddenView
	 * @see switcher\config\Config#$notFoundView
	 * @see switcher\config\Config#$errorView
	 */
	private function handleHttpException(exceptions\HttpException $e)
	{
		$this->logHttpException($e);
		
		$status = $e->getCode();
		utils\HttpHelper::setResponseStatus( $status );
		
		$this->viewName = $this->view != null ? $this->view->controller->getViewName($status) : $status;
		$this->view = $this->config->getView($this->viewName);
		
		return $this->doAction();
	}
	
	/**
	 * Log a HttpException.
	 * 
	 * @param HttpException $e The HTTP exception
	 */
	private function logHttpException(exceptions\HttpException $e)
	{
		$message = get_class($e) . ' : ' . $e->getMessage();
		$level = is_a($e, 'switcher\exceptions\InternalErrorException') ? 'error' : 'warning';
		$this->logger->$level($message);
	}
}

?>